Redis实现分布式锁 您所在的位置:网站首页 redis 分布式数据库 Redis实现分布式锁

Redis实现分布式锁

#Redis实现分布式锁| 来源: 网络整理| 查看: 265

小米信息部技术团队:https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/

我们在系统中修改已有数据时,需要先读取,然后进行修改保存,此时很容易遇到并发问题。由于修改和保存不是原子操作,在并发场景下,部分对数据的操作可能会丢失。在单服务器系统我们常用本地锁来避免并发带来的问题,然而,当服务采用集群方式部署时,本地锁无法在多个服务器之间生效,这时候保证数据的一致性就需要分布式锁来实现。image.png

分布式锁需要解决的问题

互斥性安全性死锁容错

Redis 以其高性能著称,但使用其实现分布式锁来解决并发仍存在一些困难。Redis 分布式锁只能作为一种缓解并发的手段,如果要完全解决并发问题,仍需要数据库的防并发手段。

加锁、解锁、锁超时 # 使用setnx来加锁。key是锁的唯一标识,按业务来决定命名 setnx key value # 时间复杂度O(1) # 如果key不存在,则创建并赋值, 成功返回1 # 如果key存在,失败返回0 # 当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁; # 当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败; # 使用del解锁,释放锁之后,其他线程就可以继续执行setnx命令来获得锁。 del key expire key time # 设置key的生产时间,在time时间后释放key过期会被自动删除 # 即锁超时 # SETNX 和 EXPIRE 非原子性

image.png 解释:假设一个场景中,某一个线程刚执行setnx,成功得到了锁。此时setnx刚执行成功,还未来得及执行expire命令,节点就挂掉了。此时这把锁就没有设置过期时间,别的线程就再也无法获得该锁。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传image.png 问题:不满足原子性(SETNX 和 EXPIRE 非原子性)( 如果在得到锁之后,且未执行到redisService.expire(key,expire)时挂掉,key不过期,会一直不能释放线程。资源被永远锁住,其他进程进不来)

SET key value [EX seconds] [PX milliseconds] [NX|XX] EX second:设置键的过期时间为 second 秒 PX millisecond:设置键的过期时间为 millisecond 毫秒 NX:只在键不存在时,才对键进行设置操作 XX:只在键已经存在时,才对键进行设置操作 SET操作成功完成时,返回OK,否则返回nil set locktarget ex 10 nx # 表示在locktarget键不存在时,设置过期时间为10s 解决:实现分布式锁且保证了原子性![image.png](https://img-blog.csdnimg.cn/img_convert/363246dba84142ce2cd8ce101243d286.png#averageHue=#262921&clientId=u6e48d7c0-b217-4&from=paste&height=134&id=uddddb9d0&originHeight=167&originWidth=1107&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=70225&status=done&style=none&taskId=u969f33f9-15ad-41c6-b455-7e15daf327e&title=&width=885.6) ```java RedisService redisService = SpringUtils.getBean(RedisService.class); String result result = redisService.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (result.equals("OK")){ // 执行独占资源逻辑 doOcuppiedwork(); }

大量key同时过期的注意事项:集中过期,由于清除大量的key很耗时,会出现短暂的卡顿现象

解决方案:在设置key的过期时间时,给每个key加上随机值 锁误解除

场景:如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。**解决办法:**在del释放锁之前加一个判断,验证当前的锁是不是自己加的锁。

具体在加锁的时候把当前线程的id当做value,可生成一个 UUID 标识当前线程,在删除之前验证key对应的value是不是自己线程的id。还可以使用 lua 脚本做验证标识和解锁操作。 超时解锁导致并发

场景:如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。A、B 两个线程发生并发显然是不被允许的,一般有两种方式解决该问题:

将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。 不可重入

当线程在持有锁的情况下再次请求锁,如果一个锁支持一个线程多次加锁,就是可重入锁。场景:如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。Redis可通过对锁进行重入计数,加锁时加1,减锁时减1,当计数为0时释放锁。

无法等待所释放

上述命令执行都是立即返回的,如果客户端可以等待锁释放就无法使用。

可以通过客户端轮询的方式解决该问题,当未获取到锁时,等待一段时间重新获取锁,直到成功获取锁或等待超时。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。另一种方式是使用 Redis 的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。如下:

image.png



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有